Rem
FTP helper module for mscchat client and updater, part of mscchat

Copyright (c) 2009 Michael Schaefer and Immo-Gert Birn

Mscchat is free software: you can redistribute it And/Or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, Or
(at your option) any later version.

Mscchat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY Or FITNESS For A PARTICULAR PURPOSE.  See the
GNU General Public License For more details.

You should have received a copy of the GNU General Public License
along with mscchat.  If Not, see <http://www.gnu.org/licenses/>.
EndRem

' 'FTP-Hilfsmodul
' 'Jeder Verwender benoetigt folgende Import-Anweisungen:
' Import BaH.libcurl
' Import BaH.RegEx

Const ftpserver:String = "ftp://igb.tibit.de/"
Const ftpuser:String   = "web156f3"

Const httpserver:String = "http://superchat.igb.tibit.de/"

?Win32
  Const c_version_file:String = "winversion.txt"
?Linux
  Const c_version_file:String = "linversion.txt"
?

'------------------------------------------------------------------------------
' FTP-Hilfsklasse zum einfachen Up/Download von Superchat
'------------------------------------------------------------------------------
' Hinweise:
' o Die Defaults der Klasse sind betriebssystemabhaengig.
' o Private Attribute und Methoden beginnen mit einem _ .
'------------------------------------------------------------------------------
Type TIgbFtp
  Field _pwd:String
  Field _fnames:TList = New TList

  '----------------------------------------------------------------------------
  ' Konstruktor
  '----------------------------------------------------------------------------
  ' pwd: FTP-Passwort als optionaler Parameter
  '      Wenn kein Passwort uebergeben wird, wird versucht das Passwort
  '      aus einer Datei zu lesen (siehe _get_ftp_pwd
  '----------------------------------------------------------------------------
  Method Create:TIgbFtp(pwd:String = Null)
    If pwd Then
      Self._pwd = pwd
    Else
      Self._pwd = Self._get_ftp_pwd()
    EndIf
    Return Self
  EndMethod

  '----------------------------------------------------------------------------
  ' Upload
  '----------------------------------------------------------------------------
  ' Parameter:
  '   filename : Voller Dateiname der hochzuladenden Datei
  '   version  : Versionskennung
  '   is_change_name: Soll der Name auf dem FTP-Server gemaess unserer
  '     Konvention name.version geaendet werden (default: true)            
  '   nr_old_versions_to_keep: Loesche die alten Versionen bis auf
  '     die mit diesem Parameter übergebenen; -1: nicht loeschen
  '     (default: 2)
  ' TODO: Exception
  '----------------------------------------------------------------------------
  Method upload_superchat(filename:String, version:Int = 0, is_change_name:Int = True, nr_old_versions_to_keep:Int = 2)
    Local dir:String = ExtractDir(filename)
    Local fname:String = StripDir(filename)
    Local fname_on_server:String
    If is_change_name Then
      fname_on_server = _mk_fname_on_server(fname, version)
    Else
      fname_on_server = fname
    EndIf
    ' check existence
    Local size:Long = FileSize(filename)
    If size = -1 Then Throw "Datei " + filename + " nicht gefunden!"

    Local success:Int = ChangeDir(dir)
    If Not success Throw("Konnte nicht in Verzeichnis " + dir + " wechseln.")

    Local curl:TCurlEasy = TCurlEasy.Create()
    _set_curl_user_pwd(curl)
    _set_curl_dir_or_file(curl, fname_on_server)
    curl.setOptInt(CURLOPT_UPLOAD, True)
    
    If nr_old_versions_to_keep <> -1 Then
      Local ftpcommands:String[] = _get_del_ftpcommands(fname, nr_old_versions_to_keep)
      curl.quote(ftpcommands)
    EndIf

    Local in:TStream = ReadStream(fname)
    If Not in Then Throw "Konnte " + fname + " nicht oeffnen!"

    curl.setReadStream(in)
    curl.setOptLong(CURLOPT_INFILESIZE_LARGE, size) 

    Local rc:Int = curl.perform()   
    If rc Then 
      Throw CurlError(rc)
    EndIf

    DebugLog in.pos()
    CloseStream(in)
    
  EndMethod

  '----------------------------------------------------------------------------
  ' Neueste Version herunterladen
  '----------------------------------------------------------------------------
  ' Parameter:
  '   progname: guichatclient oder chatserver
  '   to_dir  : Verzeichnis in das die heruntergeladene Datei gestellt werden 
  '             soll
  '----------------------------------------------------------------------------
  Method download_superchat(progname:String, to_dir:String)
    Local version:Int = get_version_on_server(progname)

    Local success:Int = ChangeDir(to_dir)
    If Not success Throw("Konnte nicht in Verzeichnis " + to_dir + " wechseln.")
    ' damit wissen wir dass das Verzeichnis to_dir existiert
    Local fname:String = _get_exe_fname(progname)
    Local fname_on_server:String = _mk_fname_on_server(progname, version)

    Local curl:TCurlEasy = TCurlEasy.Create()
    _set_curl_user_pwd(curl)
    _set_curl_dir_or_file(curl, fname_on_server)

    Local out:TStream = WriteStream(fname)
    If Not out Then Throw "Konnte " + fname + " nicht oeffnen!"

    curl.setWriteStream(out)

    Local rc:Int = curl.perform()   
    If rc Then 
      Throw CurlError(rc)
    EndIf

    CloseStream(out)

  EndMethod

  '----------------------------------------------------------------------------
  ' Pruefe auf neue Version auf dem FTP Server
  '----------------------------------------------------------------------------
  ' Parameter:
  '   your_version: Version des Aufrufers
  ' Rueckgabewert:
  '   True, wenn neuere Version auf dem Server existiert, ansonsten False
  ' Hinweis:
  '  Bei Fehlern wird eine Ausnahme geworfen.
  '----------------------------------------------------------------------------
  Method is_new_version_available:Int(progname:String, your_version:Int)
    If your_version < 0 Then Throw "Ungueltige Version: " + your_version
    Local ver:Int = get_version_on_server(progname)
    If ver > your_version Then Return True
    Return False
  EndMethod

  '----------------------------------------------------------------------------
  ' Hole die hoechste Versionsnummer vom FTP Server
  '----------------------------------------------------------------------------
  ' Parameter:
  '   progname: guichatclient oder chatserver
  ' Rueckgabewert:
  '   Die hoechste gefundene Version, ansonsten -1.
  ' Hinweis:
  '  Bei Fehlern wird eine Ausnahme geworfen.
  '----------------------------------------------------------------------------
  Method get_version_on_server:Int(progname:String)
    Local vers_arr:Int[] = get_versions_on_server(progname)
    Local ver:Int = -1
    If vers_arr Then ver:Int = vers_arr[ vers_arr.length - 1 ]
    DebugLog "Version von " + progname +": " + ver
    Return ver
  EndMethod

  '----------------------------------------------------------------------------
  ' Alle Datei/Verzeichnisnamen vom Server im Wurzelverzeichnis holen
  '----------------------------------------------------------------------------
  ' Liefert alle Dateinamen/Verzeichnisnamen von Objekten im Wurzelverzeichnis
  ' des FTP-Servers.
  ' Achtung: Kommt mit Bannermeldungen nicht klar!
  ' Parameter:
  '   force: Wenn True: hole auf jeden Fall die neue Dateiliste vom Server und 
  '          verwende nicht die zwischengespeicherten Daten. Default: False
  ' Rückgabewert:
  '   eine Liste mit den Dateinamen als Strings.
  '----------------------------------------------------------------------------
  Method get_fnames_on_server:TList(force:Int = False)
   
    If _fnames.isEmpty() Or force = True Then 
      Local curl:TCurlEasy = TCurlEasy.Create()
      Self._set_curl_user_pwd(curl)
      Self._set_curl_dir_or_file(curl)

      curl.setOptInt(CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_SINGLECWD) ' siehe BaH.libcurl ex_04.bmx
      curl.setWriteString()

      Local rc:Int =curl.perform()   
      If rc Then 
        Throw CurlError(rc)
      EndIf

      Local lines:String[] = curl.toString().split("~n")

      For Local line:String = EachIn lines
        DebugLog line
        line = line.Trim()
        If line Then
          Local a:String[] = line.split(" ")
          Local stmp:String = a[a.length - 1].Trim()
          If stmp Then _fnames.addlast(stmp)
        EndIf
      Next
    EndIf   
    
    ' Erzeuge eine Kopie der Daten zum Herausgeben an den Aufrufer
    Local res:TList = _fnames.copy()

    Return res
  EndMethod

  '----------------------------------------------------------------------------
  ' Hole _alle_ verfügbaren Versionsnummern vom FTP Server
  '----------------------------------------------------------------------------
  ' Parameter:
  '   progname: guichatclient oder chatserver
  ' Rueckgabewert:
  '   Alle gefundenen Versionen in einem _sortierten_ INT-array.
  ' Hinweis:
  '  Bei Fehlern wird eine Ausnahme geworfen.
  '----------------------------------------------------------------------------
  Method get_versions_on_server:Int[](progname:String)
    Local fnames:TList = get_fnames_on_server(False)
    Local reslist:TList    = New TList

    Local regex:TRegEx = TRegEx.Create("^" + _get_exe_fname(progname) + "\.(\d+)$")

    For Local fname:String = EachIn fnames
      Try
        Local match:TRegExMatch = regex.Find(fname)
        If Not match Then Continue ' naechster Schleifendurchlauf
        If match.subCount() <> 2 Then Throw "Match-Fehler " + fname
        Local stmp:String = match.SubExp(1)          
        DebugLog "Found : " + stmp
        Local tmpver:Int = Int(stmp) 
        reslist.addlast(stmp)
      Catch e:TRegExException
        Throw "Error : " + e.toString()
      End Try
    Next

    Local tmparr:Object[] = reslist.toArray()
    Local res:Int[tmparr.length]
    For Local i:Int = 0 To tmparr.length - 1
      res[i] = Int(tmparr[i].toString())
    Next
    res.sort()
    Return res

  EndMethod

  '----------------------------------------------------------------------------
  ' Loesche alte Versionen vom FTP Server
  '----------------------------------------------------------------------------
  ' Parameter:
  '   progname: guichatclient oder chatserver
  '   nr_of_versions_to_keep: die Anzahl der zu behaltenden Versionen
  '     (default ist 2)    
  ' Hinweis:
  '   o Bei Fehlern wird eine Ausnahme geworfen.
  '   o Es muß eine Operation ausgeführt werden - hier DIR-Listing.
  '----------------------------------------------------------------------------
  Method delete_old_versions(progname:String, nr_of_versions_to_keep:Int = 2)

    Local ftpcommands:String[] = _get_del_ftpcommands(progname, nr_of_versions_to_keep)

    Local curl:TCurlEasy = TCurlEasy.Create()
    Self._set_curl_user_pwd(curl)
    Self._set_curl_dir_or_file(curl)

    curl.setOptInt(CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_SINGLECWD) ' siehe BaH.libcurl ex_04.bmx
    curl.setWriteString()

    curl.quote(ftpcommands)   ' Hier werden die FTP-Kommandos uebergeben!

    Local rc:Int =curl.perform()   
    If rc And rc <> 21 Then 
      Throw CurlError(rc)
    EndIf

    curl.freelists()
    curl.cleanup()

  EndMethod
  '----------------------------------------------------------------------------
  ' Private Methoden
  '----------------------------------------------------------------------------

  Method _get_ftp_pwd:String() Private
    Local pwdfile:String
    ? Win32
    pwdfile = getenv_("USERPROFILE") + "/igbpwd.pwd"
    ?Linux
    ?
  EndMethod

  '----------------------------------------------------------------------------
  Method _mk_fname_on_server:String(f:String,v:Int = 0) Private
    Local res:String = StripDir(f)
    If v = 0 Then Return res
    Return res + "." + String(v)
  EndMethod

  '----------------------------------------------------------------------------
  Method _get_exe_fname:String(progname:String)
    ?Linux
      Return progname 
    ?Win32
      Return progname + ".exe"
    ?'Ende Betriebssystemspezifika
  EndMethod

  '----------------------------------------------------------------------------
  Method _set_curl_user_pwd(curl:TCurlEasy) Private
    curl.setOptString(CURLOPT_USERPWD, ftpuser + ":" + Self._pwd)
  EndMethod

  '----------------------------------------------------------------------------
  Method _set_curl_dir_or_file(curl:TCurlEasy, fname_on_server:String = Null)
    If fname_on_server Then
      curl.setOptString(CURLOPT_URL, ftpserver + fname_on_server)  ' Datei
    Else
      curl.setOptString(CURLOPT_URL, ftpserver)  ' Verzeichnis
    EndIf
  EndMethod

  '----------------------------------------------------------------------------
  Method _get_del_ftpcommands:String[](progname:String, nr_of_versions_to_keep:Int = 2)
  
    If nr_of_versions_to_keep < 0 Then nr_of_versions_to_keep = 2
    Local versions:Int[] = get_versions_on_server(progname)
    Local nr_versions:Int = versions.length
    If nr_versions <= nr_of_versions_to_keep Then Return Null
    Local nr_dele:Int = nr_versions - nr_of_versions_to_keep
    Local ftpcommands:String[nr_dele + 1] 'Versionsdatei muß auch gelöscht werden
    For Local i:Int = 0 To nr_dele - 1
      ' Wichtig: auch wenn das loesch-Kommando auf dem Server "delete" ist,
      ' muß hier "DELE" verwendet werden (wirl von libcurl ausgewertet)
      ftpcommands[i] = "DELE " + _mk_fname_on_server(progname, versions[i])
      DebugLog ftpcommands[i] 
    Next
    ftpcommands[nr_dele] = "DELE " + c_version_file
    Return ftpcommands

  EndMethod
EndType

'------------------------------------------------------------------------------
' HTTP-Hilfsklasse zum einfachen Download von Superchat
'------------------------------------------------------------------------------
' Hinweise:
' o Die Defaults der Klasse sind betriebssystemabhaengig.
' o Private Attribute und Methoden beginnen mit einem _ .
'------------------------------------------------------------------------------
Type TIgbHttp
  Field _version:Int

  
  Method get_version_on_server:Int()

    Local curl:TCurlEasy = TCurlEasy.Create()
    curl.setOptString(CURLOPT_URL, httpserver + c_version_file)  ' Datei
    curl.setWriteString()

    Local rc:Int =curl.perform()   
    If rc Then 
      Return -1
    EndIf
    Local res:String = curl.toString()
    DebugLog res
    Return Int(res)
    
  EndMethod
  

  Method download:String(fname:String, version:Int, to_dir:String = Null, ..
    progress_function:Int(data:Object, dltotal:Double, dlnow:Double, ultotal:Double, ulnow:Double) = Null, data:Object = Null)

    If to_dir Then
      Local success:Int = ChangeDir(to_dir)
      If Not success Throw("Konnte nicht in Verzeichnis " + to_dir + " wechseln.")
      ' damit wissen wir dass das Verzeichnis to_dir existiert
    EndIf

    Local fname_on_server:String = fname + "." + version

    Local curl:TCurlEasy = TCurlEasy.Create()
    curl.setOptString(CURLOPT_URL, httpserver + fname_on_server)  ' Datei

    Local out:TStream = WriteStream(fname_on_server)
    If Not out Then Throw "Konnte " + fname_on_server + " nicht zum Schreiben oeffnen!"

    curl.setWriteStream(out)

    If progress_function Then curl.setProgressCallback(progress_function, data)

    Local rc:Int = curl.perform()   
    If rc Then 
      Throw CurlError(rc)
    EndIf

    CloseStream(out)
    Return fname_on_server
    
  EndMethod
EndType



